package com.simple.middle.test.redis.statistic.service.impl;

import com.simple.middle.test.redis.constant.UserStatisticConstant;
import com.simple.middle.test.redis.statistic.model.data.UserSignDayData;
import com.simple.middle.test.redis.statistic.model.data.UserSignTotalAndDetail;
import com.simple.middle.test.redis.statistic.service.UserSignStatisticService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 项目: pagination
 *
 * 功能描述: 用户统计纬度
 *
 * @author: WuChengXing
 *
 * @create: 2023-05-24 18:50
 **/
@Service
@Slf4j
public class UserSignStatisticServiceImpl implements UserSignStatisticService {

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public Boolean doSign(String userId) {
        String cacheKey = UserStatisticConstant.USER_SIGN_PREFIX + userId;
        // 索引需要-1
        int point = LocalDate.now().getDayOfYear() - 1;
        return this.doSign(cacheKey, point);
    }

    public Boolean doSign(String key, int offset) {
        // 校验用户是否签到
        if (checkSign(key, offset)) {
            log.warn("用户key：{},已经签到过了", key);
            return false;
        }
        // 更新日活、周活、月活
        this.updateDayWeekAndMonth(LocalDate.now().withDayOfYear(offset + 1));
        // 索引需要-1
        return redisTemplate.opsForValue().setBit(key, offset, true);
    }

    private Boolean updateDayWeekAndMonth(LocalDate localDate) {
        // 当年的第几个月
        int monthValue = localDate.getMonthValue();
        // 当年的第几个周
        WeekFields weekFields = WeekFields.of(DayOfWeek.MONDAY, 1);
        int weeks = localDate.get(weekFields.weekOfYear());
        // 当年的第几天
        int dayOfYear = localDate.getDayOfYear();
        // 月活++
        bitFiledIncr(UserStatisticConstant.USER_SIGN_MONTH_PREFIX,
                monthStart(monthValue),
                UserStatisticConstant.MONTH_UNSIGNED_NUM);
        // 周活++
        bitFiledIncr(UserStatisticConstant.USER_SIGN_WEEK_PREFIX,
                weekStart(weeks),
                UserStatisticConstant.WEEK_UNSIGNED_NUM);
        // 日活++
        bitFiledIncr(UserStatisticConstant.USER_SIGN_DAY_PREFIX,
                dayStart(dayOfYear),
                UserStatisticConstant.DAY_UNSIGNED_NUM);
        return true;
    }

    private Long dayStart(int day) {
        // 周使用的是截取32位无符号，因为索引从0开始，这里就不需要 -1 了
        return (long) (day - 1) * UserStatisticConstant.DAY_UNSIGNED_NUM * 8;
    }

    private Long weekStart(int weeks) {
        // 周使用的是截取32位无符号，因为索引从0开始，这里就不需要 -1 了
        return (long) (weeks - 1) * UserStatisticConstant.WEEK_UNSIGNED_NUM * 8;
    }

    private Long monthStart(int month) {
        return (long) (month - 1) * UserStatisticConstant.MONTH_UNSIGNED_NUM * 8;
    }

    private Long bitFiledIncr(String key, Long start, Integer bitNum) {
        List<Long> list = redisTemplate.opsForValue()
                .bitField(key,
                        BitFieldSubCommands
                                .create()
                                .incr(BitFieldSubCommands.
                                        BitFieldType
                                        .unsigned(bitNum))
                                .valueAt(start)
                                // SAT = 截断饱和，相当于是超过这个值保留最大值 254 + 2 --> 255
                                // FAIL = 修改失败，但是值还是之前的值 254 + 2 --> 254
                                // WRAP = 直接置为 0 了 254 + 2 --> 0
                                .overflow(BitFieldSubCommands.BitFieldIncrBy.Overflow.SAT)
                                .by(1));
        if (CollectionUtils.isEmpty(list)) {
            return 0L;
        }
        return Objects.isNull(list.get(0)) ? 0L : list.get(0);
    }

    public Long yearActiveNum(int year) {
        return 0L;
    }

    public Long monthActiveNum(int month) {
        String key = UserStatisticConstant.USER_SIGN_MONTH_PREFIX;
        Long start = this.monthStart(month);
        return bitFiledGet(key, start, UserStatisticConstant.MONTH_UNSIGNED_NUM);
    }

    public Long weekActiveNum(int week) {
        String key = UserStatisticConstant.USER_SIGN_WEEK_PREFIX;
        Long start = this.weekStart(week);
        return bitFiledGet(key, start, UserStatisticConstant.WEEK_UNSIGNED_NUM);
    }

    public Long dayActiveNum(int day) {
        String key = UserStatisticConstant.USER_SIGN_DAY_PREFIX;
        Long start = this.dayStart(day);
        return bitFiledGet(key, start, UserStatisticConstant.DAY_UNSIGNED_NUM);
    }

    private Long bitFiledGet(String key, Long start, Integer bitNum) {
        List<Long> list = redisTemplate.opsForValue()
                .bitField(key,
                        BitFieldSubCommands
                                .create()
                                .get(BitFieldSubCommands.
                                        BitFieldType
                                        .unsigned(bitNum))
                                .valueAt(start));
        if (CollectionUtils.isEmpty(list)) {
            return 0L;
        }
        return Objects.isNull(list.get(0)) ? 0L : list.get(0);
    }

    @Override
    public boolean checkSign(String userId) {
        String cacheKey = UserStatisticConstant.USER_SIGN_PREFIX + userId;
        int point = LocalDate.now().getDayOfYear() - 1;
        return this.checkSign(cacheKey, point);
    }

    public boolean checkSign(String key, int offset) {
        return redisTemplate.opsForValue().getBit(key, offset);
    }

    @Override
    public boolean reSign(String userId, LocalDate localDate) {
        String cacheKey = UserStatisticConstant.USER_SIGN_PREFIX + userId;
        int point = localDate.getDayOfYear() - 1;
        return doSign(cacheKey, point);
    }

    @Override
    public UserSignTotalAndDetail getSignByDate(String userId, LocalDate startTime, LocalDate endTime) {
        UserSignTotalAndDetail totalAndDetail = new UserSignTotalAndDetail();
        String cacheKey = UserStatisticConstant.USER_SIGN_PREFIX + userId;
        int start = startTime.getDayOfYear() - 1;
        int end = endTime.getDayOfYear() - 1;
        totalAndDetail.setDayData(this.buildDetail(cacheKey, start, end, totalAndDetail));
        return totalAndDetail;
    }

    private List<UserSignDayData> buildDetail(String key, int start, int end, UserSignTotalAndDetail totalAndDetail) {
        List<UserSignDayData> res = new ArrayList<>();
        Long total = 0L;
        for (int i = start; i <= end; i++) {
            UserSignDayData userSignDayData = new UserSignDayData();
            LocalDate localDate = LocalDate.now().withDayOfYear(i + 1);
            userSignDayData.setDate(localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
            if (checkSign(key, i)) {
                total++;
                userSignDayData.setSign(true);
            } else {
                userSignDayData.setSign(false);
            }

            res.add(userSignDayData);
        }
        totalAndDetail.setTotal(total);
        return res;
    }
}
